﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using NLog;
using Skyline;

namespace QuantBox
{
    public sealed class DataRequestTask : LongRunningTask
    {
        private const int DayBarSize = 24 * 60 * 60;
        private const int MinuteBarSize = 60;

        private readonly DataCenterClient _client;
        private readonly Logger _logger;

        private static (IList<string[]> groups, string[] continuous) SplitItems(IEnumerable<string> items, int size = 100)
        {
            var result = new List<string[]>();
            var list = new List<string>(size);
            var continuous = new List<string>();

            foreach (var item in items) {
                if (Regex.IsMatch(item, "^[a-zA-Z]+(88|888|99)$")) {
                    continuous.Add(item);
                    continue;
                }
                list.Add(item);
                if (list.Count == size) {
                    result.Add(list.ToArray());
                    list.Clear();
                }
            }
            if (list.Count > 0) {
                result.Add(list.ToArray());
            }

            return (result, continuous.ToArray());
        }

        private string[]? QueryInstrumentNames(string type, DateTime date, CancellationToken ct)
        {
            _logger.Info($"查询 {type} 合约.");
            var symbols = new string[0];
            ErrorInfo? error;
            if (Info.MainInstrumentOnly && type == DataCenterConst.Future) {
                var ret = _client.GetFactorInfo(string.Empty, date, date, ct);
                if (ret.items != null) {
                    symbols = ret.items.Select(n => n.Symbol).ToArray();
                }
                error = ret.error;
            }
            else
                (symbols, error) = _client.GetInstrumentNames(type, date, ct);

            if (error != null) {
                Failed?.Invoke(this, error);
                return null;
            }
            _logger.Info($"获取合约: {symbols!.Length} 个.");
            return symbols;
        }

        private void QueryInstrument(CancellationToken ct)
        {
            foreach (var type in Info.RequestInstrumentTypes) {
                Info.InstrumentType = type;
                QueryInstrument(type, Info.DateTime1, ct);
            }
        }

        private void QueryInstrument(string type, DateTime date, CancellationToken ct)
        {
            var symbols = QueryInstrumentNames(type, date, ct);
            if (symbols == null) {
                return;
            }
            TotalNum = symbols.Length;
            var (groups, continuous) = SplitItems(symbols);
            var instruments = new List<InstrumentData>();
            while (!ct.IsCancellationRequested && groups.Count > 0) {
                _logger.Info($"查询合约信息: {string.Join(",", groups[0])}");
                var (items, error) = _client.GetInstruments(groups[0], type, ct);
                if (error != null) {
                    Failed?.Invoke(this, error);
                    return;
                }
                instruments.AddRange(items!);
                groups.RemoveAt(0);
            }

            if (continuous.Length > 0) {
                var (items, error) = _client.GetContinuousInstruments(continuous, ct);
                if (error != null) {
                    Failed?.Invoke(this, error);
                    return;
                }
                instruments.AddRange(items!);
            }

            Data = instruments.ToArray();
            if (Data != null) {
                DataReady?.Invoke(this, EventArgs.Empty);
            }
            Completed = groups.Count == 0;
        }

        private bool QueryMainDayBars(string symbol, string type, CancellationToken ct)
        {
            var (list, error) = _client.GetFactorInfo(symbol, Info.DateTime1, Info.DateTime2, ct);
            if (error != null) {
                Failed?.Invoke(this, error);
                return true;
            }
            if (list == null || list.Length == 0) {
                return false;
            }

            XApi.BarDataField[]? bars;
            (bars, error) = _client.GetMainDayBars(symbol, Info.DateTime1, Info.DateTime2, ct);
            if (error != null) {
                Failed?.Invoke(this, error);
                return true;
            }
            if (bars == null) {
                return false;
            }
            var dict = list.ToDictionary(n => n.Date);
            var action = PriceAdjust(type);
            foreach (var bar in bars) {
                if (!dict.TryGetValue(bar.OpenDateTime, out var info)) {
                    continue;
                }
                bar.Open = action(info, bar.Open);
                bar.Close = action(info, bar.Close);
                bar.High = action(info, bar.High);
                bar.Low = action(info, bar.Low);
            }
            Data = bars;
            DataReady?.Invoke(this, EventArgs.Empty);
            Completed = true;
            return true;
        }

        private void QueryDayData(CancellationToken ct)
        {
            if (Info.InstrumentType == DataCenterConst.Future) {
                var (isMain, symbol, type) = IsMain(Info.InstrumentId!);
                if (isMain) {
                    if (QueryMainDayBars(symbol, type, ct)) {
                        return;
                    }
                }
            }
            var (bars, error) = _client.GetDayBars(
                Info.InstrumentId!, 
                Info.DateTime1, 
                Info.DateTime2, 
                ct);
            if (error != null) {
                Failed?.Invoke(this, error);
                return;
            }
            Data = bars;
            if (Data != null) {
                TotalNum = Data.Length;
                DataReady?.Invoke(this, EventArgs.Empty);
            }
            Completed = true;
        }

        private bool QueryRealtimeData(CancellationToken ct)
        {
            var (ticks, error) = _client.GetFuturesRealtime(Info.InstrumentId!, ct);
            if (error != null) {
                _logger.Warn($"QueryRealtimeData, {Info.InstrumentId}, {error.ErrorMsg}");
                //Failed?.Invoke(this, error);
                //return false;
            }
            Data = ticks;
            if (Data != null) {
                DataReady?.Invoke(this, EventArgs.Empty);
            }
            return true;
        }

        private static IEnumerable<DateTime> GetWorkingDate(DateTime date1, DateTime date2)
        {
            for (var date = date1; date <= date2; date = date.AddDays(1)) {
                if (date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday) {
                    yield return date;
                }
            }
        }

        private bool QueryDataPreDay(Func<DateTime, (object[]?, ErrorInfo?)> action, CancellationToken ct)
        {
            var reptileList = new LinkedList<DateTime>(GetWorkingDate(Info.DateTime1, Info.DateTime2));
            while (!ct.IsCancellationRequested && reptileList.Count > 0) {
                Data = null;
                var date = reptileList.First;
                try {
                    ErrorInfo? error;
                    (Data, error) = action(date.Value);
                    if (error != null) {
                        Failed?.Invoke(this, error);
                        return false;
                    }
                    if (Data != null) {
                        DataReady?.Invoke(this, EventArgs.Empty);
                    }
                    reptileList.RemoveFirst();
                }
                catch (AggregateException e) {
                    var inner = e.InnerException;
                    while (inner!.InnerException != null) {
                        inner = inner.InnerException;
                    }
                    if (inner is WebException web) {
                        _logger.Info(web.Message);
                        return false;
                    }
                    if (inner is HttpRequestException http) {
                        _logger.Info($"下载 {Info.InstrumentId} {date.Value:yyyyMMdd} 数据时发生错误({http.Message}),稍后尝试重新下载");
                    }
                }
            }
            return true;
        }

        private bool QueryMainTickData(string symbol, string type, CancellationToken ct)
        {
            var (list, error) = _client.GetFactorInfo(symbol, Info.DateTime1, Info.DateTime2, ct);
            if (error != null) {
                Failed?.Invoke(this, error);
                return true;
            }
            if (list == null || list.Length == 0) {
                return false;
            }
            foreach (var info in list) {
                var (ticks, error2) = _client.GetTicks(info.Symbol, Info.InstrumentType!, info.Date, ct);
                if (error2 != null) {
                    Failed?.Invoke(this, error2);
                    return true;
                }
                if (ticks == null) {
                    continue;
                }
                var action = PriceAdjust(type, info);
                foreach (var tick in ticks) {
                    tick.LastPrice = action(tick.LastPrice);
                    if (tick.Asks.Length > 0) {
                        tick.Asks[0].Price = action(tick.Asks[0].Price);
                    }
                    if (tick.Bids.Length > 0) {
                        tick.Bids[0].Price = action(tick.Bids[0].Price);
                    }
                }
                Data = ticks;
                DataReady?.Invoke(this, EventArgs.Empty);
            }
            Completed = true;
            return true;
        }

        private static (bool, string symbol, string type) IsMain(string symbol)
        {
            var match = Regex.Match(symbol, @"([a-zA-Z]+)(\d+)");
            if (match.Success) {
                var ym = match.Groups[2].Value;
                if (ym == "99" || ym == "88" | ym == "888") {
                    return (true, match.Groups[1].Value, match.Groups[2].Value);
                }
            }
            return (false, string.Empty, string.Empty);
        }

        private void QueryTickData(CancellationToken ct)
        {
            TotalNum = 0;
            //if (Info.DateTime1 == Info.DateTime2
            //    && Info.DateTime1 == DateTime.Today) {
            //    Info.RealtimeDataRequest = true;
            //}
            var (isMain, symbol, type) = IsMain(Info.InstrumentId!);
            if (isMain) {
                if (QueryMainTickData(symbol, type, ct)) {
                    return;
                }
            }
            var ret = QueryDataPreDay(date => _client.GetTicks(Info.InstrumentId!, Info.InstrumentType!, date, ct), ct);
            if (Info.DateTime2.Date == DateTime.Today && Info.QueryRealtimeData) {
                ret = QueryRealtimeData(ct);
            }
            Completed = ret;
        }

        private void QueryData(CancellationToken ct)
        {
            if (Info.BarSize.HasValue) {
                if (Info.BarSize == DayBarSize) {
                    QueryDayData(ct);
                }
                else {
                    QueryBarData(ct);
                }
            }
            else {
                QueryTickData(ct);
            }
        }

        private void QueryBarData(CancellationToken ct)
        {
            //var size = 1L;
            //if (Info.BarSize.HasValue) {
            //    size = Info.BarSize.Value / MinuteBarSize;
            //}
            var (isMain, symbol, type) = IsMain(Info.InstrumentId!);
            if (isMain) {
                if (QueryMainMinBar(symbol, type, ct)) {
                    return;
                }
            }
            var (urls, error) = _client.GetMinuteBarFiles(
                Info.InstrumentId!, 
                Info.InstrumentType!, 
                Info.DateTime1, 
                Info.DateTime2, 
                ct);
            if (error == null && urls!.Count > 0) {
                ProcessBarFiles(urls, ct);
            }
        }

        private void ProcessBarFiles(IList<string> urls, CancellationToken ct)
        {
            foreach (var item in urls) {
                ErrorInfo? error;
                (Data, error) = _client.GetMinBar(item, ct);
                if (error != null) {
                    Failed?.Invoke(this, error);
                    return;
                }
                if (Data != null) {
                    DataReady?.Invoke(this, EventArgs.Empty);
                }
            }
            Completed = true;
        }

        private bool QueryMainMinBar(string symbol, string type, CancellationToken ct)
        {
            var (list, error) = _client.GetFactorInfo(symbol, Info.DateTime1, Info.DateTime2, ct);
            if (error != null) {
                Failed?.Invoke(this, error);
                return true;
            }
            if (list == null || list.Length == 0) {
                return false;
            }
            foreach (var info in list) {
                XApi.BarDataField[]? bars;
                (bars, error) = _client.GetMinBar(info.Symbol, Info.InstrumentType!, info.Date, ct);
                if (error != null) {
                    Failed?.Invoke(this, error);
                    return true;
                }
                if (bars == null) {
                    continue;
                }
                var action = PriceAdjust(type, info);
                foreach (var bar in bars) {
                    bar.Open = action(bar.Open);
                    bar.Close = action(bar.Close);
                    bar.High = action(bar.High);
                    bar.Low = action(bar.Low);
                }
                Data = bars;
                DataReady?.Invoke(this, EventArgs.Empty);
            }
            Completed = true;
            return true;
        }

        private Func<MainFactorInfo, double, double> PriceAdjust(string type)
        {
            var decimals = 2;
            if (Info.PriceFormat != null && Info.PriceFormat.Length > 1) {
                int.TryParse(Info.PriceFormat.Substring(1, 1), out decimals);
            }

            Func<MainFactorInfo, double, double> action = type switch
            {
                "88" => (info, v) => Math.Round(v + info.AddFactor, decimals),
                "888" => (info, v) => Math.Round(v * info.DivFactor, decimals),
                _ => (info, v) => v
            };

            return action;
        }

        private Func<double, double> PriceAdjust(string type, MainFactorInfo info)
        {
            var decimals = 2;
            if (Info.PriceFormat != null && Info.PriceFormat.Length > 1) {
                int.TryParse(Info.PriceFormat.Substring(1, 1), out decimals);
            }

            Func<double, double> action = type switch
            {
                "88" => v => Math.Round(v + info.AddFactor, decimals),
                "888" => v => Math.Round(v * info.DivFactor, decimals),
                _ => v => v
            };

            return action;
        }

        protected override void Run(CancellationToken ct)
        {
            try {
                if (Info.Action == DataRequestAction.DataRequest) {
                    QueryData(ct);
                }
                else if (Info.Action == DataRequestAction.InstrumentRequest) {
                    QueryInstrument(ct);
                }
            }
            catch (OperationCanceledException) {
            }
            catch (Exception ex) {
                _logger.Error(ex);
                Failed?.Invoke(this, new ErrorInfo(ex.Message));
            }
            Done?.Invoke(this, EventArgs.Empty);
        }

        public DataRequestTask(DataRequestInfo info, DataCenterClient client, Logger logger)
        {
            _client = client;
            _logger = logger;
            Info = info;
        }

        public DataRequestInfo Info { get; }
        public object[]? Data { get; private set; }
        public int TotalNum { get; private set; }
        public bool Completed { get; private set; }

        public event EventHandler? DataReady;
        public event EventHandler? Done;
        public event EventHandler<ErrorInfo>? Failed;
    }
}
